/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
 */
package org.sbbs.dao;

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.hibernate.Hibernate;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.RevisionNumber;
import org.hibernate.envers.RevisionTimestamp;

import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.AuditQuery;
import org.hibernate.envers.query.criteria.AuditProperty;
import org.hibernate.envers.query.order.AuditOrder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.envers.repository.support.DefaultRevisionMetadata;
import org.springframework.data.history.AnnotationRevisionMetadata;
import org.springframework.data.history.Revision;
import org.springframework.data.history.RevisionMetadata;
import org.springframework.data.history.RevisionSort;
import org.springframework.data.history.Revisions;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.jpa.support.PageableUtils;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.util.Assert;
import org.hibernate.envers.RevisionType;
import static org.hibernate.envers.RevisionType.ADD;
import static org.hibernate.envers.RevisionType.DEL;
import static org.hibernate.envers.RevisionType.MOD;
import org.sbbs.entity.BaseEntity;
import static org.springframework.data.history.RevisionMetadata.RevisionType.DELETE;
import static org.springframework.data.history.RevisionMetadata.RevisionType.INSERT;
import static org.springframework.data.history.RevisionMetadata.RevisionType.UNKNOWN;
import static org.springframework.data.history.RevisionMetadata.RevisionType.UPDATE;

/**
 *
 * @author jenkins
 */
/**
 * @Author:ShadowSaint
 * @Date:19-4-4 上午3:50
 * @Description: TODO
 */
public class BaseRepositoryImpl<T extends BaseEntity, ID extends Serializable>
        extends SimpleJpaRepository<T, ID>
        implements BaseRepo<T, ID> {

    private final EntityManager entityManager;
    private final JpaEntityInformation<T, ?> entityInformation;

    public BaseRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager em) {
        super(entityInformation, em);
        this.entityInformation = entityInformation;
        this.entityManager = em;
    }

    @Override
    public Page<Long> findAllIds(Pageable pageable) {
        String hql = "select id from " + this.getDomainClass().getName();
        TypedQuery<Long> query = this.entityManager.createQuery(hql, Long.class);
        if (pageable.isPaged()) {
            query.setFirstResult(PageableUtils.getOffsetAsInteger(pageable));
            query.setMaxResults(pageable.getPageSize());
        }
        return PageableExecutionUtils.getPage(query.getResultList(), pageable, () -> executeCountQuery(getCountQuery(null, this.getDomainClass())));
    }

    private static long executeCountQuery(TypedQuery<Long> query) {
        Assert.notNull(query, "TypedQuery must not be null");
        List<Long> totals = query.getResultList();
        long total = 0L;
        for (Long element : totals) {
            total += element == null ? 0 : element;
        }
        return total;
    }

    @SuppressWarnings("unchecked")
    public Optional<Revision<Long, T>> findLastChangeRevision(ID id) {

        List<Object[]> singleResult = createBaseQuery(id) //
                .addOrder(AuditEntity.revisionProperty("timestamp").desc()) //
                .addOrder(AuditEntity.revisionNumber().desc()) //
                .setMaxResults(1) //
                .getResultList();

        Assert.state(singleResult.size() <= 1, "We expect at most one result.");

        if (singleResult.isEmpty()) {
            return Optional.empty();
        }

        return Optional.of(createRevision(new QueryResult<>(singleResult.get(0))));
    }

    @Override
    @SuppressWarnings("unchecked")
    public Optional<Revision<Long, T>> findRevision(ID id, Long revisionNumber) {

        Assert.notNull(id, "Identifier must not be null!");
        Assert.notNull(revisionNumber, "Revision number must not be null!");

        List<Object[]> singleResult = (List<Object[]>) createBaseQuery(id) //
                .add(AuditEntity.revisionNumber().eq(revisionNumber)) //
                .getResultList();

        Assert.state(singleResult.size() <= 1, "We expect at most one result.");

        if (singleResult.isEmpty()) {
            return Optional.empty();
        }

        return Optional.of(createRevision(new QueryResult<>(singleResult.get(0))));
    }

    @SuppressWarnings("unchecked")
    public Revisions<Long, T> findRevisions(ID id) {

        List<Object[]> resultList = createBaseQuery(id).getResultList();
        List<Revision<Long, T>> revisionList = new ArrayList<>(resultList.size());

        for (Object[] objects : resultList) {
            revisionList.add(createRevision(new QueryResult<>(objects)));
        }

        return Revisions.of(revisionList);
    }

    private AuditOrder mapRevisionSort(RevisionSort revisionSort) {

        return RevisionSort.getRevisionDirection(revisionSort).isDescending() //
                ? AuditEntity.revisionNumber().desc() //
                : AuditEntity.revisionNumber().asc();
    }

    private List<AuditOrder> mapPropertySort(Sort sort) {

        if (sort.isEmpty()) {
            return Collections.singletonList(AuditEntity.revisionNumber().asc());
        }

        List<AuditOrder> result = new ArrayList<>();
        for (Sort.Order order : sort) {

            AuditProperty<Object> property = AuditEntity.property(order.getProperty());
            AuditOrder auditOrder = order.getDirection().isAscending() //
                    ? property.asc() //
                    : property.desc();

            result.add(auditOrder);
        }

        return result;
    }

    @SuppressWarnings("unchecked")
    public Page<Revision<Long, T>> findRevisions(ID id, Pageable pageable) {

        AuditQuery baseQuery = createBaseQuery(id);

        List<AuditOrder> orderMapped = (pageable.getSort() instanceof RevisionSort revisionSort)
                ? List.of(mapRevisionSort(revisionSort))
                : mapPropertySort(pageable.getSort());

        orderMapped.forEach(baseQuery::addOrder);

        List<Object[]> resultList = baseQuery //
                .setFirstResult((int) pageable.getOffset()) //
                .setMaxResults(pageable.getPageSize()) //
                .getResultList();

        Long count = (Long) createBaseQuery(id) //
                .addProjection(AuditEntity.revisionNumber().count()).getSingleResult();

        List<Revision<Long, T>> revisions = new ArrayList<>();

        for (Object[] singleResult : resultList) {
            revisions.add(createRevision(new QueryResult<>(singleResult)));
        }
        return new PageImpl<>(revisions, pageable, count);
    }

    private AuditQuery createBaseQuery(ID id) {

        Class<T> type = entityInformation.getJavaType();
        AuditReader reader = AuditReaderFactory.get(entityManager);

        return reader.createQuery() //
                .forRevisionsOfEntity(type, false, true) //
                .add(AuditEntity.id().eq(id));
    }

    @SuppressWarnings("unchecked")
    private Revision<Long, T> createRevision(QueryResult<T> queryResult) {
        return Revision.of((RevisionMetadata<Long>) queryResult.createRevisionMetadata(), queryResult.entity);
    }

    @SuppressWarnings("unchecked")
    static class QueryResult<T> {

        private final T entity;
        private final Object metadata;
        private final RevisionMetadata.RevisionType revisionType;

        QueryResult(Object[] data) {

            Assert.notNull(data, "Data must not be null");
            Assert.isTrue( //
                    data.length == 3, //
                    () -> String.format("Data must have length three, but has length %d.", data.length));
            Assert.isTrue( //
                    data[2] instanceof RevisionType, //
                    () -> String.format("The third array element must be of type Revision type, but is of type %s",
                            data[2].getClass()));

            entity = (T) data[0];
            metadata = data[1];
            revisionType = convertRevisionType((RevisionType) data[2]);
        }

        RevisionMetadata<?> createRevisionMetadata() {

            return metadata instanceof DefaultRevisionEntity defaultRevisionEntity //
                    ? new DefaultRevisionMetadata(defaultRevisionEntity, revisionType) //
                    : new AnnotationRevisionMetadata<>(Hibernate.unproxy(metadata), RevisionNumber.class, RevisionTimestamp.class,
                            revisionType);
        }

        private static RevisionMetadata.RevisionType convertRevisionType(RevisionType datum) {

            return switch (datum) {
                case ADD ->
                    INSERT;
                case MOD ->
                    UPDATE;
                case DEL ->
                    DELETE;
                default ->
                    UNKNOWN;
            };
        }
    }

}
